/* 
    WhaleSpotter
    Copyright (C) 2007  Bob Mottram
    fuzzgun@gmail.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace whalespotter
{
    public class whale
    {
        // unique identifiers
        public String Name;
        public String SerialNumber;
        public String Sex;
        public int YearBorn;
        public String Maturity;
        public String Species;
        public bool useForIdentification;

        // an index which can be used to quickly find similar individuals
        public Byte[] Index;

        // average texture of the tail
        public int Texture;

        // features based upon available observations
        public ArrayList keyFeatures;
        public ArrayList linearFeatures;
        public ArrayList linearFeaturesAll;
        public bool[] edge_pattern;

        // list of sightings
        public ArrayList Sightings = new ArrayList();

        #region "loading and saving"

        /// <summary>
        /// save the whale data to file
        /// </summary>
        /// <param name="binfile"></param>
        public void Save(BinaryWriter binfile)
        {
            binfile.Write(Name);
            binfile.Write(SerialNumber);
            binfile.Write(Sex);
            binfile.Write(YearBorn);
            binfile.Write(Maturity);
            binfile.Write(Species);
            binfile.Write(Texture);
            binfile.Write(useForIdentification);

            for (int i = 0; i < 20 * 2; i++)
                binfile.Write(Index[i]);

            binfile.Write(keyFeatures.Count);
            for (int i = 0; i < keyFeatures.Count; i++)
            {
                tailPoint pt = (tailPoint)keyFeatures[i];
                pt.Save(binfile);
            }

            binfile.Write(linearFeatures.Count);
            for (int i = 0; i < linearFeatures.Count; i++)
            {
                ArrayList lf = (ArrayList)linearFeatures[i];
                binfile.Write(lf.Count);
                for (int j = 0; j < lf.Count; j++)
                {
                    tailPoint pt = (tailPoint)lf[j];
                    pt.Save(binfile);
                }
            }

            binfile.Write(Sightings.Count);
            for (int i = 0; i < Sightings.Count; i++)
            {
                sighting s = (sighting)Sightings[i];
                s.Save(binfile);
            }

            binfile.Write(edge_pattern.Length);
            for (int i = 0; i < edge_pattern.Length; i++)
                binfile.Write(edge_pattern[i]);
        }

        /// <summary>
        /// export the sighting data in CSV format for export to spreadsheet
        /// </summary>
        /// <returns></returns>
        public String exportCSV()
        {
            String csv = "";

            for (int i = 0; i < Sightings.Count; i++)
            {
                String csv_line = Species + ", " + Name + ", " + SerialNumber + ", " +
                                  Sex + ", " + Convert.ToString(YearBorn) + ", " + Maturity + ", ";
                sighting s = (sighting)Sightings[i];
                csv_line += s.Observer + ", " +
                            Convert.ToString(s.lattitude) + ", " +
                            Convert.ToString(s.longitude) + ", " +
                            Convert.ToString(s.heading) + ", " + 
                            s.date.ToString() + ", " +
                            s.TailFilename + ", " +
                            s.ImageFilename + ", ";
                csv += csv_line + System.Environment.NewLine;
            }

            return (csv);
        }


        public static String exportCSVTitle()
        {
            String csv = "Common Name, Species, Name, Serial, Sex, Year Of Birth, Maturity, Observer, Lattitude, Longitude, Heading, Date, Tail Filename, Sighting Filename, ";
            csv += System.Environment.NewLine;
            return (csv);
        }


        /// <summary>
        /// load data from file
        /// </summary>
        /// <param name="binfile"></param>
        public void Load(BinaryReader binfile)
        {
            Name = binfile.ReadString();
            SerialNumber = binfile.ReadString();
            Sex = binfile.ReadString();
            YearBorn = binfile.ReadInt32();
            Maturity = binfile.ReadString();
            Species = binfile.ReadString();
            Texture = binfile.ReadInt32();
            useForIdentification = binfile.ReadBoolean();

            Index = new Byte[200 * 2];
            for (int i = 0; i < 20 * 2; i++)
                Index[i] = binfile.ReadByte();

            int no_of_key_features = binfile.ReadInt32();
            keyFeatures = new ArrayList();
            for (int i = 0; i < no_of_key_features; i++)
            {
                tailPoint pt = new tailPoint(0, 0);
                pt.Load(binfile);
                keyFeatures.Add(pt);
            }

            linearFeatures = new ArrayList();
            linearFeaturesAll = new ArrayList();
            int no_of_linear_features = binfile.ReadInt32();
            for (int i = 0; i < no_of_linear_features; i++)
            {
                ArrayList lf = new ArrayList();
                int length = binfile.ReadInt32();
                for (int j = 0; j < length; j++)
                {
                    tailPoint pt = new tailPoint(0, 0);
                    pt.Load(binfile);
                    lf.Add(pt);
                    linearFeaturesAll.Add(pt);
                }
                linearFeatures.Add(lf);
            }

            Sightings = new ArrayList();
            int no_of_sightings = binfile.ReadInt32();
            for (int i = 0; i < no_of_sightings; i++)
            {
                sighting s = new sighting();
                s.Load(binfile);
                s.individual = this;
                Sightings.Add(s);
            }

            try
            {
                int pattern_length = binfile.ReadInt32();
                edge_pattern = new bool[pattern_length];
                for (int i = 0; i < pattern_length; i++)
                    edge_pattern[i] = binfile.ReadBoolean();
            }
            catch
            {
            }
        }

        #endregion

        #region "constructor"

        public whale()
        {
        }

        public whale(String Observer, String Name, String SerialNumber, String Sex, int YearBorn, String Maturity,
                     String Species, int[,,] Index, int texture,
                     ArrayList keyFeatures, ArrayList linearFeatures, bool[] edgePattern,
                     String lattitude, String longitude, float heading, 
                     String filename, String tailFilename, bool useForIdentification,
                     String notes, ArrayList manual_points,
                     int day, int month, int year, int hour, int min)
        {
            this.Name = Name;
            this.SerialNumber = SerialNumber;
            this.Species = Species;
            this.Sex = Sex;
            this.YearBorn = YearBorn;
            this.Maturity = Maturity;
            this.useForIdentification = useForIdentification;
            this.edge_pattern = edgePattern;            
            AddSighting(Observer, lattitude, longitude, heading, filename, tailFilename,
                        Index, texture, keyFeatures, linearFeatures, useForIdentification, notes,
                        manual_points, day, month, year, hour, min);

            this.Index = new Byte[20 * 2];
            int n = 0;
            if (Index != null)
            {
                for (int x = 0; x < 20; x++)
                {
                    for (int y = 0; y < 2; y++)
                    {
                        this.Index[n] = (Byte)Index[x, y, 2];
                        n++;
                    }
                }
            }
        }

        #endregion

        #region "matching"

        /// <summary>
        /// returns a score based upon matching tail edge features
        /// </summary>
        /// <param name="edges"></param>
        /// <returns></returns>
        public int edgePatternScore(bool[] edges)
        {
            int tollerance = edge_pattern.Length / 10;
            int hits = 0;
            int score = 0;
            if (edge_pattern != null)
            {
                for (int i = 0; i < edge_pattern.Length; i++)
                {
                    if (edge_pattern[i])
                    {
                        hits++;
                        int min = 0;
                        int max = edge_pattern.Length / 2;
                        if (i > edge_pattern.Length / 2)
                        {
                            min = edge_pattern.Length / 2;
                            max = edge_pattern.Length;
                        }

                        int closest = -999;
                        for (int j = i - tollerance; j < i + tollerance; j++)
                        {
                            if ((j >= min) && (j < max))
                            {
                                if (edges[j])
                                {
                                    int dist = i - j;
                                    if (dist < 0) dist = -dist;
                                    if ((dist < closest) || (closest == -999))
                                    {
                                        closest = dist;                                        
                                    }
                                }
                            }
                        }
                        if (closest != -999)
                        {
                            int diff = tollerance - closest;
                            score += (diff * diff);
                        }
                    }
                }
            }
            if (hits > 0) score = score * 10 * 150 / hits;
            return (score);
        }


        /// <summary>
        /// returns a score based upon key feature matching
        /// </summary>
        /// <param name="features"></param>
        /// <returns></returns>
        public int keyFeatureScore(ArrayList features, bool useLinearFeatures)
        {
            int score = 0;
            int radius_x = 10;
            int radius_y = 10;
            int max_orient_diff = 40;
            int hits = 0;

            ArrayList feats = keyFeatures;
            if (useLinearFeatures) feats = linearFeaturesAll;

            for (int i = 0; i < features.Count; i++)
            {
                tailPoint pt = (tailPoint)features[i];
                if (pt.densityHistogram != null)
                {
                    int Type = pt.Type;
                    int orient = pt.Orientation;

                    int j = 0;
                    bool matched = false;
                    int winner = -1;
                    int max_score = -1;
                    while ((j < feats.Count) && (!matched))
                    {
                        tailPoint pt2 = (tailPoint)feats[j];
                        if ((pt2.Type == Type) && (pt2.densityHistogram != null))
                        {
                            int dx = pt2.x - pt.x;
                            if ((dx > -radius_x) && (dx < radius_x))
                            {
                                int dy = pt2.y - pt.y;
                                if ((dy > -radius_y) && (dy < radius_y))
                                {
                                    int dorient = orient - pt2.Orientation;
                                    if (dorient < 0) dorient = 360 + dorient;
                                    if (dorient > 180) dorient = 360 - dorient;
                                    if (dorient < max_orient_diff)
                                    {
                                        // compare the density histograms
                                        int density_diff = 0;
                                        int density_hits = 0;
                                        for (int k = 0; k < 3; k++)
                                        {
                                            for (int l = 0; l < tailPoint.densityHistogramLevels; l++)
                                            {
                                                if ((pt.densityHistogram[k, l] > 0) ||
                                                    (pt2.densityHistogram[k, l] > 0))
                                                {
                                                    int dd = Math.Abs(pt.densityHistogram[k, l] -
                                                                      pt2.densityHistogram[k, l]);
                                                    density_diff += dd;
                                                    density_hits++;
                                                }
                                            }
                                        }
                                        if (density_hits > 0)
                                        {
                                            int sc2 = 30 - density_diff;
                                            if (sc2 < 1) sc2 = 1;

                                            int sc = (((radius_x - Math.Abs(dx)) +
                                                     (radius_y - Math.Abs(dy))) * 4) +
                                                     (max_orient_diff - dorient);
                                            sc = sc * sc2 / density_hits;

                                            if ((max_score == -1) || (sc > max_score))
                                            {
                                                max_score = sc;
                                                winner = j;
                                                //matched = true;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        j++;
                    }

                    if (max_score != -1)
                    {
                        hits++;
                        score += max_score;
                    }
                }
            }
            if (hits > 0) score = (score * 1000 * hits / features.Count);

            return (score);
        }

        /// <summary>
        /// return a matching score based upon the index
        /// </summary>
        /// <param name="Index"></param>
        /// <returns></returns>
        public int matchingScore(int[, ,] Index)
        {
            int score = 0;
            int n = 0;
            for (int x = 0; x < 20; x++)
            {
                for (int y = 0; y < 2; y++)
                {
                    if (Index[x, y, 2] > 0)
                    {
                        if (this.Index[n] == (Byte)Index[x, y, 2])
                            score++;
                    }
                    n++;
                }
            }
            return (score);
        }


        #endregion

        #region "model updating"

        /// <summary>
        /// remove low scoring features, leaving only the strongest surviving
        /// </summary>
        private void nonmax_supress(int max_features)
        {
            ArrayList victims = new ArrayList();

            for (int i = 0; i < keyFeatures.Count; i++)
            {
                tailPoint pt = (tailPoint)keyFeatures[i];
                for (int j = 0; j < keyFeatures.Count; j++)
                {
                    tailPoint pt2 = (tailPoint)keyFeatures[j];
                    if (pt2 != pt)
                    {
                        if (pt2.intensity < pt.intensity)
                        {
                            int dx = pt2.x - pt.x;
                            if ((dx > -5) && (dx < 5))
                            {
                                int dy = pt2.y - pt.y;
                                if ((dy > -5) && (dy < 5))
                                    victims.Add(pt2);
                            }
                        }
                    }
                }
            }
            for (int i = 0; i < victims.Count; i++)
                keyFeatures.Remove(victims[i]);

            while (keyFeatures.Count > max_features)
            {
                int lowest = 0;
                int victim = -1;
                for (int i = 0; i < keyFeatures.Count; i++)
                {
                    tailPoint pt = (tailPoint)keyFeatures[i];
                    if ((i == 0) || (pt.intensity < lowest))
                    {
                        lowest = pt.intensity;
                        victim = i;
                    }
                }
                if (victim > -1)
                    keyFeatures.RemoveAt(victim);
            }
        }


        private void updateLinearFeatures()
        {
            linearFeatures.Clear();
            linearFeaturesAll.Clear();
            if (Sightings.Count == 1)
            {
                // only one observations has been made
                ArrayList features = ((sighting)Sightings[0]).linearFeatures;
                for (int i = 0; i < features.Count; i++)
                {
                    ArrayList lf = (ArrayList)features[i];
                    linearFeatures.Add(lf);
                    for (int j = 0; j < lf.Count; j++)
                    {
                        tailPoint pt = (tailPoint)lf[j];
                        linearFeaturesAll.Add(pt);
                    }
                }
            }
            else
            {
                // merge multiple sightings to create a model

                // TODO: better modeling may be possible.  This is just a quick method

                // add all features to the set
                for (int i = 0; i < Sightings.Count; i++)
                {
                    sighting s = (sighting)Sightings[i];
                    for (int j = 0; j < s.linearFeatures.Count; j++)
                    {
                        ArrayList lf = (ArrayList)s.linearFeatures[j];
                        linearFeatures.Add(lf);
                        for (int k = 0; k < lf.Count; k++)
                        {
                            tailPoint pt = (tailPoint)lf[k];
                            linearFeaturesAll.Add(pt);
                        }
                    }
                }
            }
        }


        /// <summary>
        /// generate a set of key features by merging features observed over multiple sightings
        /// </summary>
        private void updateKeyFeatures()
        {
            keyFeatures.Clear();

            if (Sightings.Count == 1)
            {
                // only one observations has been made
                ArrayList feat = ((sighting)Sightings[0]).keyFeatures;
                for (int i = 0; i < feat.Count; i++)
                {
                    tailPoint pt = (tailPoint)feat[i];
                    keyFeatures.Add(pt);
                }
            }
            else
            {
                // merge multiple sightings to create a model

                // TODO: better modeling may be possible.  This is just a quick method

                // add all features to the set
                for (int i = 0; i < Sightings.Count; i++)
                {
                    sighting s = (sighting)Sightings[i];
                    for (int j = 0; j < s.keyFeatures.Count; j++)
                    {
                        tailPoint pt = (tailPoint)s.keyFeatures[j];
                        keyFeatures.Add(pt);
                    }
                }

                // perform non-maximal supression
                nonmax_supress(30);
            }
        }


        /// <summary>
        /// update the index based upon available sightings
        /// </summary>
        private void updateIndex()
        {
            Texture = 0;
            int[,] hits = new int[20 * 2, 3];
            for (int i = 0; i < Sightings.Count; i++)
            {
                sighting s = (sighting)Sightings[i];
                Texture += s.Texture;
                for (int j = 0; j < 20 * 2; j++)
                {
                    int idx = s.Index[j];
                    hits[j, idx]++;
                }
            }
            Texture /= Sightings.Count;
            for (int j = 0; j < 20 * 2; j++)
            {
                int max = -1;
                int winner = 0;
                for (int k = 1; k < 3; k++)
                {
                    if (hits[j, k] > max)
                    {
                        max = hits[j, k];
                        winner = k;
                    }
                }
                Index[j] = (Byte)winner;
            }
        }

        #endregion

        #region "new sightings"

        /// <summary>
        /// add a new sighting for this individual
        /// </summary>
        /// <param name="lattitude"></param>
        /// <param name="longitude"></param>
        /// <param name="filename"></param>
        public void AddSighting(String observer, String lattitude, String longitude, float heading,
                                String filename, String tailFilename, int[, ,] Index, int texture, 
                                ArrayList keyFeatures, 
                                ArrayList linearFeatures,
                                bool useForIdentification, String notes,
                                ArrayList manual_points,
                                int day, int month, int year, int hour, int min)
        {
            sighting new_sighting = new sighting();
            new_sighting.individual = this;
            new_sighting.Observer = observer;
            new_sighting.heading = heading;
            new_sighting.lattitude = util.MapToFloat(lattitude);
            new_sighting.longitude = util.MapToFloat(longitude);
            new_sighting.ImageFilename = filename;
            new_sighting.TailFilename = tailFilename;
            new_sighting.useForIdentification = useForIdentification;
            new_sighting.Notes = notes;
            new_sighting.manual_points = manual_points;
            if (year != 0)
                new_sighting.date = new DateTime(year, month, day, hour, min, 0);

            this.keyFeatures = new ArrayList();
            this.linearFeatures = new ArrayList();
            this.linearFeaturesAll = new ArrayList();
            Byte[] idx = new Byte[20 * 2];
            this.Index = new Byte[20 * 2];
            int n = 0;
            if (Index != null)
            {
                for (int x = 0; x < 20; x++)
                {
                    for (int y = 0; y < 2; y++)
                    {
                        idx[n] = (Byte)Index[x, y, 2];
                        n++;
                    }
                }
            }
            new_sighting.Index = idx;
            new_sighting.Texture = texture;
            new_sighting.keyFeatures = keyFeatures;
            new_sighting.linearFeatures = linearFeatures;
            new_sighting.linearFeaturesAll = linearFeaturesAll;
            Sightings.Add(new_sighting);
            
            updateIndex();
            updateKeyFeatures();
            updateLinearFeatures();
        }

        #endregion

        #region "display"

        /// <summary>
        /// show a pattern representing the tail trailing edge
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        public void showEdgePattern(Byte[] img, int img_width, int img_height, bool[] compare)
        {
            for (int i = 0; i < img_width * img_height * 3; i++)
                img[i] = 0;

            if (edge_pattern != null)
            {
                for (int i = 0; i < edge_pattern.Length; i++)
                {
                    if (edge_pattern[i])
                    {
                        int x_start = i * img_width / edge_pattern.Length;
                        int x_end = (i+1) * img_width / edge_pattern.Length;
                        if (x_end >= img_width) x_end = img_width - 1;
                        for (int y = 0; y < img_height; y++)
                        {
                            for (int x = x_start; x < x_end; x++)
                            {
                                int n = ((y * img_width) + x) * 3;
                                bool matched = false;
                                if (compare[i] == edge_pattern[i]) matched = true;
                                if (i > 0)
                                    if (compare[i - 1] == edge_pattern[i]) matched = true;
                                if (i < edge_pattern.Length-2)
                                    if (compare[i + 1] == edge_pattern[i]) matched = true;

                                if (matched)
                                    img[n+1] = (Byte)255;
                                else
                                    img[n] = (Byte)255;
                            }
                        }
                    }
                }
            }
        }

        #endregion
    }
}
